package com.willpower.eureka.config;

import com.willpower.eureka.auth.MyInvalidSessionStrategy;
import com.willpower.eureka.controller.ValidateCodeFilter;
import com.willpower.eureka.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import javax.sql.DataSource;

/**
 * @author: wen-yi;
 * @date: 2021/10/29 23:18;
 * @Description:
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PersistentTokenRepository tokenRepository;
    /**
     * 成功的处理方案
     */
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    /**
     * 失败的处理方案
     */
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;
    /**
     * 未登录的处理方案
     */
    @Autowired
    private AuthenticationEntryPoint myAuthenticationEntryPoint;
    /**
     * 权限不足处理方案
     */
    @Autowired
    private AccessDeniedHandler myAccessDeniedHandler;
    /**
     * 注销成功处理器
     */
    @Autowired
    private LogoutSuccessHandler myLogoutSuccessHandler;
    /**
     * 注销操作处理器
     */
    @Autowired
    private LogoutHandler myLogoutHandler;
    /**
     * Session超时处理
     */
    @Autowired
    private InvalidSessionStrategy myInvalidSessionStrategy;
    /**
     * 被挤下线时的操作
     */
    @Autowired
    private SessionInformationExpiredStrategy myExpiredSessionStrategy;
    /**
     * 添加过滤器 用于验证验证码
     */
    @Autowired
    private ValidateCodeFilter validateCodeFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//.ignoringAntMatchers("/eureka/**")
                // 为了添加验证码验证
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                    .formLogin().permitAll()
                    //.loginPage("/login.html") //前后端分离便不再需要控制页面了
                    .loginPage("http://localhost:8080/login")
                    // 自定义登录逻辑
                    .loginProcessingUrl("/login")
                    //自定义登录用户名参数
                    .usernameParameter("username")
                    .passwordParameter("password")
                    // 必须是POST
                    //默认认证成功后 默认跳转的路径
                    //.defaultSuccessUrl("main.html")
                    //.successForwardUrl("/")
                    //.failureForwardUrl("/login?error")
                    //自定义登陆成功逻辑 前后端分离 json返回
                    .successHandler(myAuthenticationSuccessHandler)
                    .failureHandler(myAuthenticationFailureHandler)
                .and()
                    //自定义
                    .exceptionHandling()
                    //403异常处理 无权限方案
                    .accessDeniedHandler(myAccessDeniedHandler)
                    //未登录方案
                    .authenticationEntryPoint(myAuthenticationEntryPoint)
                .and()
                    .logout().permitAll()
                    //.logoutSuccessUrl("/login.html")
                    .logoutUrl("/logout")
                    .addLogoutHandler(myLogoutHandler)
                    .logoutSuccessHandler(myLogoutSuccessHandler)
                    .deleteCookies("OAUTH2-CLIENT-SESSIONID-11000")
                .and()
                    .rememberMe()
                    //自定义参数
                    .rememberMeParameter("rememberMe")
                    //自定义失效时间 默认两周
                    //.tokenValiditySeconds()
                    //自定义功能实现逻辑
                    //.rememberMeServices()
                    //自定义登录逻辑
                    .userDetailsService(userService)
                    //数据存储位置
                    .tokenRepository(tokenRepository)
                .and()
                    //所有的请求都将被拦截
                    .authorizeRequests()
                    //放行的地址 ant表达式 ?:一个字符 *:多个字符 **:多个目录  放行所有
                    .antMatchers("/captcha","/login","/logout","/oauth/**","/eureka/**","/api/**","/login.html").access("permitAll()")
                    // 正则表达式 一般不用 写角色的时候 不能写ROLE_   也可以基于IP控制 127.0.0.1
                    .regexMatchers(HttpMethod.GET, ".+[.]jpg").hasAuthority("admin")
                    //spring.mvc.servlet.path = /api  访问/images不用加前缀 一般不用
                    //.mvcMatchers("/images/**").servletPath("/api").permitAll()
                    //所有的请求必须被认证（登录） 配置控制
                    .anyRequest().authenticated()
                    //.anyRequest().access("@accessServer.hasPermission(request,authentication)")
                .and()
                    .sessionManagement()
                    // 会话管理 ALWAYS|IF_REQUIRED(默认)|NEVER|STATELESS(前后端分离无状态应用|节省资源)
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    //登录超时
                    .invalidSessionStrategy(myInvalidSessionStrategy)
                    //最大登录数为1
                    .maximumSessions(1)
                    //false:允许别人登录  true:不允许别人登录
                    .maxSessionsPreventsLogin(false)
                    //被挤下线处理
                    .expiredSessionStrategy(myExpiredSessionStrategy);

        // 关闭csrf  跨域伪造请求  除了携带sision id 还得携带服务器给的_csrf Token
        // http.csrf().disable();
        //super.configure(http);
    }

    /**
     * 可以在内存中构建虚拟的用户名密码
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /*
        PasswordEncoder passwordEncoder = passwordEncoder();
        auth.inMemoryAuthentication()
                .withUser("root")
                .password(passwordEncoder.encode("123456"))
                .roles("user")
                .and()
                .passwordEncoder(passwordEncoder);
        //super.configure(auth);
        */
        PasswordEncoder passwordEncoder = passwordEncoder();
        auth.userDetailsService(userService)
                .passwordEncoder(passwordEncoder);
    }

    @Bean
    @Override //用户密码模式
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 推荐使用 BCryptPasswordEncoder进行加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置记住我 数据存储的位置
     * @return
     */
    @Bean
    public PersistentTokenRepository tokenRepository(){
        //一般选择 存储jdbc 而不是内存
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
        tokenRepository.setDataSource(dataSource);
        //启动时是否创建表，第一次要,之后注释掉
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
}
